﻿
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;

namespace Effects
{
    public sealed class ParticleEngine
    {      
        private volatile static ParticleEngine instance;
        private static object syncRoot = new Object();

        private List<ParticleEffect> activeEffects;
        public Texture2D[] textures { get; set; }

        private ParticleEngine() 
        {
            this.activeEffects = new List<ParticleEffect>();
        }

        public static ParticleEngine GetInstance()
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                        instance = new ParticleEngine();
                }
            }
            return instance;
        }

        public void StartEffect(EffectFactory.EffectType _type, Vector2 _pos, int _drawOrder)
        {
            StartEffect(_type, _pos, Vector2.Zero, 1.0f, 1.0f, _drawOrder);
        }

        public void StartEffect(EffectFactory.EffectType _type, Vector2 _pos, Vector2 _vel, float _scale, float _speed, int _depth)
        {
            ParticleEffect effect = EffectFactory.Make(_type);
           
            if (effect != null)
            {               
                effect.pos = _pos;
                effect.scale = _scale;
                effect.speed = _speed;
                effect.depth = _depth;

                activeEffects.Add(effect);
            }
        }

        public void Update()
        {
            for (UInt16 i = 0; i < activeEffects.Count; ++i)
            {
                ParticleEffect effect = activeEffects[i] as ParticleEffect;
                effect.Update();
                if (!effect.bActive)
                    activeEffects.RemoveAt(i);
            }
        }

        public void Draw(SpriteBatch _spriteBatch)
        {
            for (UInt16 i = 0; i < activeEffects.Count; ++i)
            {                
                (activeEffects[i] as ParticleEffect).Draw(_spriteBatch);
            }
        }
    }


    public abstract class ParticleEffect
    {
        // TODO: colourisation support
        protected struct Particle
        {
            public bool bActive;
            public Vector2 pos;
            public Vector2 vel;
            public Vector2 accel;
            public float angle;
            public float angleMod;
            public float scale;
            public float scaleMod;
            public float scaleMax;
            public Texture2D texture;
            public float alpha;
            public float alphaMod;
            public float red;
            public float green;
            public float blue;
            public float redMod;
            public float greenMod;
            public float blueMod;

            public float delayInFrames;     // these are floats instead of ints so we can fuck with time dilation
            public float timeoutInFrames;   //

            public int velMult;
            public int velDiv;

            // C# won't allow parameterless constructors. This will have to do!
            public void setDefaults()
            {               
                pos = Vector2.Zero;
                vel = Vector2.Zero;
                accel = Vector2.Zero;
                angle = 0.0f;
                angleMod = 0.0f;
                scale = 1.0f;
                scaleMod = 0.0f;
                scaleMax = 10.0f;
                texture = null;
                delayInFrames = 0;
                timeoutInFrames = 0;
                velMult = 1;
                velDiv = 1;
                alpha = 1.0f;
                alphaMod = 0.0f;
                red = 1.0f;
                green = 1.0f;
                blue = 1.0f;
                redMod = 0.0f;
                greenMod = 0.0f;
                blueMod = 0.0f;
            }
        }
        protected Particle[] p;

        public float speed { get; set; }
        public float scale { get; set; }        
        public float depth { get; set; }
        public Vector2 pos { get; set; }
        public Vector2 vel { get; set; }
        public bool bActive { get; set; }       // just tells us whether the effect has finished


        protected float angle;
        protected float angleMod;
      
        protected abstract void Init();

        public ParticleEffect()
        {
            // default effect-wide properties
            this.pos = vel = Vector2.Zero;
            this.scale = 1.0f;
            this.speed = 1.0f;
            this.angle = 0;
            this.angleMod = 0;

            Init();
        }

        public void Update()
        {
            this.bActive = false;

            if (p != null)
            {
                this.pos += this.vel * this.speed;
                this.angle += this.angleMod * this.speed;

                for (UInt32 i=0; i < p.Length; ++i)
                {
                    if (p[i].delayInFrames > 0)
                    {
                        // frame countdown until particle is made active  (eg, useful for things like fountains)                  
                        p[i].delayInFrames -= this.speed;
                        if (p[i].delayInFrames <= 0)
                            p[i].bActive = true;
                    }
                    else if (p[i].timeoutInFrames > 0)
                    {
                        p[i].pos += p[i].vel * this.speed;
                        p[i].vel += p[i].accel * this.speed;
                        p[i].angle += p[i].angleMod * this.speed;
                        p[i].scale += p[i].scaleMod * this.speed;
                        MyMaths.bound(ref p[i].scale, 0.0f, p[i].scaleMax);
                        p[i].timeoutInFrames -= this.speed;
                        p[i].alpha += p[i].alphaMod * this.speed;
                        MyMaths.bound(ref p[i].alpha, 0.0f, 1.0f);
                        p[i].vel *= p[i].velMult;
                        if (p[i].velDiv != 0) // avoid /0
                            p[i].vel /= p[i].velDiv;

                        p[i].red += p[i].redMod * this.speed;
                        MyMaths.bound(ref p[i].red, 0.0f, 1.0f);
                        p[i].green += p[i].greenMod * this.speed; ;
                        MyMaths.bound(ref p[i].green, 0.0f, 1.0f);
                        p[i].blue += p[i].blueMod * this.speed; ;
                        MyMaths.bound(ref p[i].blue, 0.0f, 1.0f);

                        if (!this.bActive)
                            this.bActive = true;         // effect still busy doing something
                    }
                }
            }
        }

        public void Draw(SpriteBatch _spriteBatch)
        {
            if (p != null)
            {
                for (int i = p.Length; --i >= 0; )
                {
                    if (p[i].timeoutInFrames > 0
                        && p[i].delayInFrames <= 0
                        && p[i].texture != null)
                    {
                        // translate pos for effect angle
                        Vector2 point = p[i].pos;
                        MyMaths.rotateVector(ref point, this.angle);

                        _spriteBatch.Draw(
                            p[i].texture,
                            this.pos + point,
                            null,
                            new Color(p[i].red, p[i].green, p[i].blue, p[i].alpha),
                            p[i].angle,
                            new Vector2(p[i].texture.Width / 2, p[i].texture.Height / 2),
                            p[i].scale,
                            SpriteEffects.None,
                            this.depth
                         );
                    }
                }
            }
        }
   
        // initialise an array with default values 
        protected void CreateParticleArray(int _nParticles)
        {
            p = new Particle[_nParticles];

            for (UInt16 i = 0; i < p.Length; ++i)
            {
                //Particle p = new Particle();                
                p[i].setDefaults();
            }
        }

        /* Some handy shorthand functions */
        protected float randf()
        {
            return (float)MyMaths.rand.NextDouble();
        }

        protected float randf(float max)
        {
            return (float)MyMaths.rand.NextDouble() * max;
        }

        protected int rand()
        {
            return MyMaths.rand.Next();
        }

        protected int rand(int max)
        {
            return MyMaths.rand.Next(max);
        }
    }


    public static class EffectFactory
    {
        public enum EffectType
        {
            EXPLODE = 0,
            //CIRCLE_BURST,
            FOUNTAIN,
            CIRCLE_FOUNTAIN,
            CIRCLE_FOUNTAIN2,
            POWER_FROM_ETHER,
            PORTAL,
            WTF,


            NUM_EFFECTS  
        }

        public static ParticleEffect Make(EffectType _type)
        {
            ParticleEffect pe = null;
            switch (_type)
            {
                case EffectType.EXPLODE:
                    pe = new EffectLibrary.ExplosionEffect();
                    break;

                //case EffectType.CIRCLE_BURST:
                //    pe = new EffectLibrary.CircleBurst();
                //    break;

                case EffectType.FOUNTAIN:
                    pe = new EffectLibrary.Fountain();
                    break;


                case EffectType.CIRCLE_FOUNTAIN:
                    pe = new EffectLibrary.CircleFountain();
                    break;

                case EffectType.POWER_FROM_ETHER:
                    pe = new EffectLibrary.PowerFromEther();
                    break;

                case EffectType.PORTAL:
                    pe = new EffectLibrary.Portal();
                    break;

                case EffectType.WTF:
                    pe = new EffectLibrary.Wtf();
                    break;
            }

            return pe;
        }
    }

    #region EffectLibrary

    /***************************************************************/
    /*  This is where you make your effects                        */
    /***************************************************************/
    namespace EffectLibrary
    {

        public class ExplosionEffect : ParticleEffect
        {
            protected override void Init()
            {
                CreateParticleArray((rand() % 20) + 20);

                for (UInt16 i = 0; i < p.Length; ++i)
                {                       
                    p[i].vel.X = -5.0f + (randf() * 10);
                    p[i].vel.Y = -5.0f + (randf() * 10);
                    p[i].angleMod = (float)MyMaths.DegToRad(randf(5.0f));
                    p[i].scaleMod = (randf() - 0.5f) / 50;
                    p[i].timeoutInFrames = 50 + (rand() % 50);
                    p[i].texture = ParticleEngine.GetInstance().textures[rand() % 3];
                    
                    p[i].velMult = 60;
                    p[i].velDiv = 61;
                }
            }
        }

        public class CircleBurst : ParticleEffect
        {
            protected override void Init()
            {
                CreateParticleArray(16);
                 
                this.angleMod = (float)MyMaths.DegToRad(0.5f);

                float spacing = (float)MyMaths.DegToRad(360.0f / p.Length);
                for (UInt16 i = 0; i < p.Length; ++i)
                {                   
                    p[i].vel.X = (float)Math.Sin(i * spacing);
                    p[i].vel.Y = (float)Math.Cos(i * spacing);
                    p[i].angleMod = (float)MyMaths.DegToRad((i % 2) * 3.0f);
                    
                    p[i].timeoutInFrames = 300;
                    p[i].texture = ParticleEngine.GetInstance().textures[1 + (i % 2)];
                    p[i].alphaMod = -0.005f;

                    
                }
            }
        }


        public class Fountain : ParticleEffect
        {
            protected override void Init()
            {
                CreateParticleArray(50);

                for (UInt16 i = 0; i < p.Length; ++i)
                {
                    p[i].vel.X = 1.0f + randf(1.5f);
                    p[i].vel.Y = -4.0f - randf(3);
                    p[i].angleMod = (float)MyMaths.DegToRad((-5 + rand(10)));
                    p[i].timeoutInFrames = rand(200);
                    p[i].delayInFrames = i * 2;
                    p[i].texture = ParticleEngine.GetInstance().textures[(i % 3)];
                    p[i].alphaMod = -0.005f;
                    p[i].accel.Y = 0.1f;
                    p[i].scale = 0.0f;
                    p[i].scaleMod = 0.2f;
                    p[i].scaleMax = 0.7f;

                }
            }
        }

        public class CircleFountain : ParticleEffect
        {
            protected override void Init()
            {
                CreateParticleArray(100);

                float spacing = (float)MyMaths.DegToRad(1080 / p.Length);
                for (UInt16 i = 0; i < p.Length; ++i)
                {                   
                    p[i].vel.X = (float)Math.Sin(i * spacing);
                    p[i].vel.Y = (float)Math.Cos(i * spacing);
                    p[i].angleMod = (float)MyMaths.DegToRad(5);
                    p[i].timeoutInFrames = 100;
                    p[i].delayInFrames = i;
                    p[i].texture = ParticleEngine.GetInstance().textures[1];
                    p[i].alphaMod = -0.005f;
                    p[i].scale = 0.0f;
                    p[i].scaleMod = 0.1f;
                    p[i].scaleMax = 0.5f;

                }
            }        
        }

        public class PowerFromEther : ParticleEffect
        {
            protected override void Init()
            {
                CreateParticleArray(100);
                              
                for (UInt16 i = 0; i < p.Length; ++i)
                {
                    int radius = 200;
                    p[i].pos = new Vector2(-radius + rand(radius * 2), -radius + rand(radius * 2));

                    p[i].timeoutInFrames = 50;

                    p[i].vel.X = -p[i].pos.X / p[i].timeoutInFrames;
                    p[i].vel.Y = -p[i].pos.Y / p[i].timeoutInFrames;                    
                   
                    p[i].delayInFrames = rand(p.Length);
                    p[i].texture = ParticleEngine.GetInstance().textures[2];
                    p[i].alpha = 0.0f;
                    p[i].alphaMod = 1.0f / p[i].timeoutInFrames;
                    p[i].scaleMod = -1.0f / p[i].timeoutInFrames;

                    p[i].greenMod = -0.02f;
                    p[i].blueMod = -0.005f;

                }
            }
        }

        public class Portal : ParticleEffect
        {
            protected override void Init()
            {
                CreateParticleArray(200);

                float spacing = (float)MyMaths.DegToRad(1080 / p.Length);
                for (UInt16 i = 0; i < p.Length; ++i)
                {
                    p[i].timeoutInFrames = 200;
                    p[i].angleMod = (float)MyMaths.DegToRad(5);
                    p[i].texture = ParticleEngine.GetInstance().textures[i % 2];
                    p[i].scaleMax = 0.6f;
                    p[i].delayInFrames = i;
                    p[i].alphaMod = -0.007f;
                    p[i].scale = 0.0f;
                    p[i].scaleMod = 0.008f;
                    p[i].redMod = -0.01f;
                    if (i % 2 == 0)
                    {
                        p[i].vel.X = (float)Math.Sin(i * spacing ) / 2;//+ MyMaths.DegToRad(90));
                        p[i].vel.Y = -(float)Math.Cos(i * spacing );//+ MyMaths.DegToRad(90));
                        p[i].pos.X = p[i].vel.X *10;
                        p[i].pos.Y = p[i].vel.Y *10;
                    }
                    else
                    {
                        p[i].vel.X = (float)Math.Sin(-i * spacing) / 2;
                        p[i].vel.Y = (float)Math.Cos(-i * spacing);
                        p[i].pos.X = p[i].vel.X * 10;
                        p[i].pos.Y = p[i].vel.Y * 10;                        
                    }               
                }
            }
        }
        
        
        public class Wtf : ParticleEffect
        {
            protected override void Init()
            {
                CreateParticleArray(200);

                float spacing = (float)MyMaths.DegToRad(1080 / p.Length);
                for (UInt16 i = 0; i < p.Length; ++i)
                {
                    p[i].timeoutInFrames = 200;
                    p[i].angleMod = (float)MyMaths.DegToRad(5);
                    p[i].texture = ParticleEngine.GetInstance().textures[i % 2];
                    p[i].scaleMax = 0.4f;
                    p[i].delayInFrames = i;
                    p[i].alphaMod = -0.007f;
                    p[i].scale = 0.0f;
                    p[i].scaleMod = 0.006f;

                    if (i % 2 == 0)
                    {
                        p[i].vel.X = (float)Math.Sin(i * spacing + MyMaths.DegToRad(20));
                        p[i].vel.Y = -(float)Math.Cos(i * spacing);
                        p[i].pos.X = p[i].vel.X / 2;// *10;
                        p[i].pos.Y = p[i].vel.Y * 100;
                        p[i].redMod = -0.005f;
                    }
                    else
                    {
                        p[i].vel.X = (float)Math.Sin(-i * spacing) / 2;
                        p[i].vel.Y = (float)Math.Cos(-i * spacing);
                        p[i].pos.X = p[i].vel.X * 100;
                        p[i].pos.Y = p[i].vel.Y / 2;// *10;
                        p[i].greenMod = -0.005f;
                    }
                    p[i].pos /= 2;
                    p[i].vel /= 2;
                }
            }
        }
        
    }

    #endregion


}

